/*
 * ST7789V.c
 *
 *  Created on: Aug 6, 2020
 *      Author: kychu
 */

#include <sys/param.h>
#include "st7789v.h"
#include "esp_log.h"
#include "driver/gpio.h"
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/xtensa_api.h"
#include "freertos/task.h"

static const char *TAG = "st7789v";

#define LCD_CMD_LEVEL   (0)
#define LCD_DATA_LEVEL  (1)

/*
 This struct stores a bunch of command values to be initialized for ST7789V
*/
typedef struct {
    uint8_t cmd;
    uint8_t data[16];
    uint8_t databytes; //No of data in data; bit 7 = delay after set; 0xFF = end of cmds.
} lcd_init_cmd_t;

//Place data into DRAM. Constant data gets placed into DROM by default, which is not accessible by DMA.
DRAM_ATTR static const lcd_init_cmd_t lcd_init_cmds[] = {
    /* Memory Data Access Control, MX=MV=1, MY=ML=MH=0, RGB=0 */
    {0x36, {(1 << 5) | (1 << 6)}, 1},
    /* Interface Pixel Format, 16bits/pixel for RGB/MCU interface */
	{0x3A, {0x55}, 1},
    /* Porch Setting */
    {0xB2, {0x0C, 0x0C, 0x00, 0x33, 0x33}, 5},
    /* Gate Control, Vgh=13.65V, Vgl=-10.43V */
	{0xB7, {0x35}, 1},
#if !CONFIG_LCD_COLOR_BIG_ENDIAN
	/* Little Endian (LSB first) */
	{0xB0, {0x00, 0xC8}, 2},
#endif /* CONFIG_LCD_COLOR_BIG_ENDIAN */
    /* VCOM Setting, VCOM=1.175V */
	{0xBB, {0x19}, 1},
    /* LCM Control, XOR: BGR, MX, MH */
    {0xC0, {0x2C}, 1},
    /* VDV and VRH Command Enable, enable=1 */
	{0xC2, {0x01}, 1},
    /* VRH Set, Vap=4.4+... */
	{0xC3, {0x12}, 1},
    /* VDV Set, VDV=0 */
    {0xC4, {0x20}, 1},
    /* Frame Rate Control, 60Hz, inversion=0 */
    {0xC6, {0x0f}, 1},
    /* Power Control 1, AVDD=6.8V, AVCL=-4.8V, VDDS=2.3V */
    {0xD0, {0xA4, 0xA1}, 1},
    /* Positive Voltage Gamma Control */
	{0xE0, {0xD0, 0x04, 0x0D, 0x11, 0x13, 0x2B, 0x3F, 0x54, 0x4C, 0x18, 0x0D, 0x0B, 0x1F, 0x23}, 14},
    /* Negative Voltage Gamma Control */
	{0xE1, {0xD0, 0x04, 0x0C, 0x11, 0x13, 0x2C, 0x3F, 0x44, 0x51, 0x2F, 0x1F, 0x1F, 0x20, 0x23}, 14},
	{0x21, {0}, 0x80},
    /* Sleep Out */
    {0x11, {0}, 0x80},
    /* Display On */
    {0x29, {0}, 0x80},
    {0, {0}, 0xff}
};

//This function is called (in irq context!) just before a transmission starts. It will
//set the D/C line to the value indicated in the user field.
static void lcd_spi_pre_transfer_callback(spi_transaction_t *t)
{
  lcd_dc_t *dc = (lcd_dc_t *) t->user;
  gpio_set_level((int)dc->dc_io, (int)dc->dc_level);
}

esp_err_t lcd_cmd(spi_device_handle_t spi, uint16_t cmd, lcd_dc_t *dc)
{
  dc->dc_level = LCD_CMD_LEVEL;
  spi_transaction_t t = {
    .length = 8,                     // Command is 8 bits
    .tx_buffer = &cmd,               // The data is the cmd itself
    .user = (void *) dc,             // D/C needs to be set to 0
  };
  return spi_device_polling_transmit(spi, &t); // Transmit!
}

esp_err_t lcd_data(spi_device_handle_t spi, const void *data, int len, lcd_dc_t *dc)
{
  if (len <= 0) {
    return ESP_ERR_INVALID_ARG;    //no need to send anything
  }
  dc->dc_level = LCD_DATA_LEVEL;

  spi_transaction_t t = {
    .length = len * 8,                         // Len is in bytes, transaction length is in bits.
    .tx_buffer = data,                         // Data
    .user = (void *) dc,                       // D/C needs to be set to 1
  };
  return spi_device_polling_transmit(spi, &t); // Transmit!
}

esp_err_t lcd_init(lcd_conf_t* lcd_conf, spi_device_handle_t *spi_dev, lcd_dc_t *dc, int dma_chan)
{
  //Initialize non-SPI GPIOs
  gpio_pad_select_gpio(lcd_conf->pin_num_dc);
  gpio_set_direction(lcd_conf->pin_num_dc, GPIO_MODE_OUTPUT);

  //Reset the display
  if (lcd_conf->pin_num_rst < GPIO_NUM_MAX) {
    gpio_pad_select_gpio(lcd_conf->pin_num_rst);
    gpio_set_direction(lcd_conf->pin_num_rst, GPIO_MODE_OUTPUT);
    gpio_set_level(lcd_conf->pin_num_rst, (lcd_conf->rst_active_level) & 0x1);
    vTaskDelay(100 / portTICK_RATE_MS);
    gpio_set_level(lcd_conf->pin_num_rst, (~(lcd_conf->rst_active_level)) & 0x1);
    vTaskDelay(100 / portTICK_RATE_MS);
  }

  if (lcd_conf->init_spi_bus) {
    //Initialize SPI Bus for LCD
    spi_bus_config_t buscfg = {
      .miso_io_num = lcd_conf->pin_num_miso,
      .mosi_io_num = lcd_conf->pin_num_mosi,
      .sclk_io_num = lcd_conf->pin_num_clk,
      .quadwp_io_num = -1,
      .quadhd_io_num = -1,
    };
    spi_bus_initialize(lcd_conf->spi_host, &buscfg, dma_chan);
  }

  spi_device_interface_config_t devcfg = {
    // Use low speed to read ID.
    .clock_speed_hz = lcd_conf->clk_freq,     //Clock out frequency
    .mode = 0,                                //SPI mode 0
    .spics_io_num = lcd_conf->pin_num_cs,     //CS pin
    .queue_size = 7,                          //We want to be able to queue 7 transactions at a time
	.flags = SPI_DEVICE_3WIRE | SPI_DEVICE_HALFDUPLEX,
    .pre_cb = lcd_spi_pre_transfer_callback,  //Specify pre-transfer callback to handle D/C line
  };

  spi_bus_add_device(lcd_conf->spi_host, &devcfg, spi_dev);

  int cmd = 0;
  assert(lcd_init_cmds != NULL);
  //Send all the commands
  while (lcd_init_cmds[cmd].databytes != 0xff) {
    lcd_cmd(*spi_dev, lcd_init_cmds[cmd].cmd, dc);
    lcd_data(*spi_dev, lcd_init_cmds[cmd].data, lcd_init_cmds[cmd].databytes & 0x0F, dc);
    if (lcd_init_cmds[cmd].databytes & 0x80) {
      vTaskDelay(100 / portTICK_RATE_MS);
    }
    cmd ++;
  }

  //Enable backlight GPIO
  if (lcd_conf->pin_num_bckl < GPIO_NUM_MAX) {
  	gpio_pad_select_gpio(lcd_conf->pin_num_bckl);
  	gpio_set_direction(lcd_conf->pin_num_bckl, GPIO_MODE_OUTPUT);
  	// default off
  	gpio_set_level(lcd_conf->pin_num_bckl, (~(lcd_conf->bckl_active_level)) & 0x1);
  }

  ESP_LOGI(TAG, "ST7789V Initialized.");
  return ESP_OK;
}

void lcd_fast_fill(spi_device_handle_t *spi, uint16_t *data, uint16_t xs, uint16_t xe, uint16_t ys, uint16_t ye)
{
  esp_err_t ret;
  int x;

  //Transaction descriptors. Declared static so they're not allocated on the stack; we need this memory even when this
  //function is finished because the SPI driver needs access to it even while we're already calculating the next line.
  static spi_transaction_t trans[6];

  //In theory, it's better to initialize trans and data only once and hang on to the initialized
  //variables. We allocate them on the stack, so we need to re-init them each call.
  for (x = 0; x < 6; x ++) {
	memset(&trans[x], 0, sizeof(spi_transaction_t));
	if ((x & 1) == 0) {
	  //Even transfers are commands
	  trans[x].length = 8;
	  trans[x].user = (void *)0;
	} else {
	  //Odd transfers are data
	  trans[x].length = 8 * 4;
	  trans[x].user = (void *)1;
	}
	trans[x].flags = SPI_TRANS_USE_TXDATA;
  }
  trans[0].tx_data[0] = 0x2A;                        //Column Address Set
  trans[1].tx_data[0] = xs >> 8;                     //Start Col High
  trans[1].tx_data[1] = xs & 0xff;                   //Start Col Low
  trans[1].tx_data[2] = xe >> 8;                     //End Col High
  trans[1].tx_data[3] = xe & 0xff;                   //End Col Low
  trans[2].tx_data[0] = 0x2B;                        //Page address set
  trans[3].tx_data[0] = ys >> 8;                     //Start page high
  trans[3].tx_data[1] = ys & 0xff;                   //start page low
  trans[3].tx_data[2] = ye >> 8;                     //end page high
  trans[3].tx_data[3] = ye & 0xff;                   //end page low
  trans[4].tx_data[0] = 0x2C;                        //memory write
  trans[5].tx_buffer = data;                         //finally send the data
  trans[5].length = (xe - xs + 1) * (ye - ys + 1) * 2 * 8;   //Data length, in bits
  trans[5].flags = 0;                                //undo SPI_TRANS_USE_TXDATA flag

  //Queue all transactions.
  for (x = 0; x < 6; x ++) {
	ret = spi_device_queue_trans(spi, &trans[x], portMAX_DELAY);
	assert(ret == ESP_OK);
  }

  //When we are here, the SPI driver is busy (in the background) getting the transactions sent. That happens
  //mostly using DMA, so the CPU doesn't have much to do here. We're not going to wait for the transaction to
  //finish because we may as well spend the time calculating the next line. When that is done, we can call
  //send_line_finish, which will wait for the transfers to be done and check their status.
}

void lcd_fast_fill_finish(spi_device_handle_t *spi)
{
  spi_transaction_t *rtrans;
  esp_err_t ret;
  //Wait for all 6 transactions to be done and get back the results.
  for (int x = 0; x < 6; x ++) {
	ret = spi_device_get_trans_result(spi, &rtrans, portMAX_DELAY);
	assert(ret == ESP_OK);
	//We could inspect rtrans now if we received any info back. The LCD is treated as write-only, though.
  }
}

void lcd_window(spi_device_handle_t *spi, uint16_t xs, uint16_t xe, uint16_t ys, uint16_t ye, lcd_dc_t *dc)
{
  dc->dc_level = LCD_CMD_LEVEL;
  spi_transaction_t t;
  memset(&t, 0, sizeof(spi_transaction_t));
  t.user = (void *) dc,                 // D/C line
  t.flags = SPI_TRANS_USE_TXDATA,
  t.length = 8,                         // Command is 8 bits
  t.tx_data[0] = 0x2A,
  spi_device_polling_transmit(spi, &t); // Transmit!
  dc->dc_level = LCD_DATA_LEVEL;
  t.length = 8 * 4,                     // Command is 8 bits
  t.tx_data[0] = xs >> 8;               //Start Col High
  t.tx_data[1] = xs & 0xff;             //Start Col Low
  t.tx_data[2] = xe >> 8;               //End Col High
  t.tx_data[3] = xe & 0xff;             //End Col Low
  spi_device_polling_transmit(spi, &t); // Transmit!
  t.length = 8,                         // Command is 8 bits
  t.tx_data[0] = 0x2B,
  spi_device_polling_transmit(spi, &t); // Transmit!
  dc->dc_level = LCD_DATA_LEVEL;
  t.length = 8 * 4,                     // Command is 8 bits
  t.tx_data[0] = ys >> 8;               //Start Col High
  t.tx_data[1] = ys & 0xff;             //Start Col Low
  t.tx_data[2] = ye >> 8;               //End Col High
  t.tx_data[3] = ye & 0xff;             //End Col Low
  spi_device_polling_transmit(spi, &t); // Transmit!
  t.length = 8,                         // Command is 8 bits
  t.tx_data[0] = 0x2C,
  spi_device_polling_transmit(spi, &t); // Transmit!
}

void lcd_disp_on(lcd_conf_t* lcd_conf)
{
  if (lcd_conf->pin_num_bckl < GPIO_NUM_MAX) {
	gpio_set_level(lcd_conf->pin_num_bckl, (lcd_conf->bckl_active_level) & 0x1);
  }
}

void lcd_disp_off(lcd_conf_t* lcd_conf)
{
  if (lcd_conf->pin_num_bckl < GPIO_NUM_MAX) {
    gpio_set_level(lcd_conf->pin_num_bckl, (~(lcd_conf->bckl_active_level)) & 0x1);
  }
}

uint32_t lcd_get_id(void)
{
  return 0x77897789;
}
